ADSORPTION
Overview
The ADSORPTION function fits experimental data to established adsorption isotherm models using nonlinear least squares regression. Adsorption isotherms describe how molecules (adsorbates) bind to solid surfaces (adsorbents) at constant temperature, and are fundamental to applications in catalysis, chromatography, water purification, and materials science. For more background on adsorption phenomena, see the Wikipedia article on Adsorption.
This implementation uses scipy.optimize.curve_fit from the SciPy library to perform nonlinear regression via the Levenberg-Marquardt algorithm. The function supports six adsorption models:
Brunauer-Emmett-Teller (BET) Multilayer: Extends the Langmuir model to account for multilayer adsorption on surfaces. Developed in 1938 by Brunauer, Emmett, and Teller, this model is widely used for surface area measurements. See BET theory.
BET Simplified: A simplified form of the BET equation for specific applications.
Freundlich Extended Variable Exponent: An empirical model with a variable exponent that accommodates heterogeneous surfaces and non-ideal adsorption behavior. The Freundlich isotherm is one of the earliest adsorption models (1906).
Gunary Phosphate Soil Adsorption: A specialized model for describing phosphate adsorption in soil systems, commonly used in agricultural and environmental science.
Langmuir Extended Fractional Power: A modification of the classic Langmuir adsorption model that incorporates a fractional power term for improved flexibility.
Langmuir Extended Reciprocal Form: Another Langmuir variant expressed in reciprocal form with an additional exponent parameter.
The function returns optimized parameter values along with their standard errors (derived from the covariance matrix), enabling assessment of fit quality. The classic Langmuir isotherm takes the form:
\theta = \frac{K_{eq} \cdot p}{1 + K_{eq} \cdot p}
where \theta is surface coverage and K_{eq} is the equilibrium constant.
This example function is provided as-is without any representation of accuracy.
Excel Usage
=ADSORPTION(xdata, ydata, adsorption_model)
xdata(list[list], required): The xdata valueydata(list[list], required): The ydata valueadsorption_model(str, required): The adsorption_model value
Returns (list[list]): 2D list [param_names, fitted_values, std_errors], or error string.
Examples
Example 1: BET multilayer adsorption fitting
Inputs:
| adsorption_model | xdata | ydata |
|---|---|---|
| brunauer_emmett_teller_multilayer | 0.1 | 0.411991191769355 |
| 1.3250000000000002 | -11.066489754739134 | |
| 2.5500000000000003 | -4.0922716541134605 | |
| 3.7750000000000004 | -3.019941985751676 | |
| 5 | -2.93122469020847 |
Excel formula:
=ADSORPTION("brunauer_emmett_teller_multilayer", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.411991191769355;-11.066489754739134;-4.0922716541134605;-3.019941985751676;-2.93122469020847})
Expected output:
| a | b |
|---|---|
| 2.765 | 1.068 |
| 0.04572 | 0.0176 |
Example 2: BET simplified adsorption fitting
Inputs:
| adsorption_model | xdata | ydata |
|---|---|---|
| brunauer_emmett_teller_simplified | 0.1 | 0.04031302521196009 |
| 1.3250000000000002 | -0.5250296303974683 | |
| 2.5500000000000003 | -0.12597014605985662 | |
| 3.7750000000000004 | -0.06481495021686037 | |
| 5 | -0.0597407326617247 |
Excel formula:
=ADSORPTION("brunauer_emmett_teller_simplified", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.04031302521196009;-0.5250296303974683;-0.12597014605985662;-0.06481495021686037;-0.0597407326617247})
Expected output:
| a | b |
|---|---|
| 2.155 | 2.092 |
| 0.3519 | 0.6067 |
Example 3: Freundlich extended exponent fitting
Inputs:
| adsorption_model | xdata | ydata |
|---|---|---|
| freundlich_extended_variable_exponent | 0.1 | 0.03760293065948473 |
| 1.3250000000000002 | 3.373393804529696 | |
| 2.5500000000000003 | 3.3816488352097918 | |
| 3.7750000000000004 | 3.1005998529945566 | |
| 5 | 3.068996503762253 |
Excel formula:
=ADSORPTION("freundlich_extended_variable_exponent", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.03760293065948473;3.373393804529696;3.3816488352097918;3.1005998529945566;3.068996503762253})
Expected output:
| a | b | c |
|---|---|---|
| 2.76 | 1.218 | 1.881 |
| 0.1373 | 0.2544 | 0.1751 |
Example 4: Gunary phosphate soil adsorption fitting
Inputs:
| adsorption_model | xdata | ydata |
|---|---|---|
| gunary_phosphate_soil_adsorption | 0.1 | 0.03338292967433101 |
| 1.3250000000000002 | 0.22376144054819436 | |
| 2.5500000000000003 | 0.31582488704502965 | |
| 3.7750000000000004 | 0.36772354465575596 | |
| 5 | 0.4225541696491852 |
Excel formula:
=ADSORPTION("gunary_phosphate_soil_adsorption", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.03338292967433101;0.22376144054819436;0.31582488704502965;0.36772354465575596;0.4225541696491852})
Expected output:
| a | b | c |
|---|---|---|
| 2.051 | 0.9498 | 2.293 |
| 0.6418 | 0.2719 | 0.8488 |
Example 5: Langmuir fractional power fitting
Inputs:
| adsorption_model | xdata | ydata |
|---|---|---|
| langmuir_extended_fractional_power | 0.1 | 2.369638270382609 |
| 1.3250000000000002 | 1.3014159334217155 | |
| 2.5500000000000003 | 0.9663780982339676 | |
| 3.7750000000000004 | 0.7438174832905579 | |
| 5 | 0.6698187406430091 |
Excel formula:
=ADSORPTION("langmuir_extended_fractional_power", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {2.369638270382609;1.3014159334217155;0.9663780982339676;0.7438174832905579;0.6698187406430091})
Expected output:
| a | b | c |
|---|---|---|
| 2.679 | 1.186 | 1.811 |
| 0.07742 | 0.1149 | 0.05272 |
Example 6: Langmuir reciprocal form fitting
Inputs:
| adsorption_model | xdata | ydata |
|---|---|---|
| langmuir_extended_reciprocal_form | 0.1 | 0.3423934299926415 |
| 1.3250000000000002 | 0.2510648378075721 | |
| 2.5500000000000003 | 0.2080267307806113 | |
| 3.7750000000000004 | 0.1762085052698202 | |
| 5 | 0.16104444468838883 |
Excel formula:
=ADSORPTION("langmuir_extended_reciprocal_form", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.3423934299926415;0.2510648378075721;0.2080267307806113;0.1762085052698202;0.16104444468838883})
Expected output:
| a | b | c |
|---|---|---|
| 2.763 | 0.9796 | 1.795 |
| 0.0391 | 0.06371 | 0.04309 |
Python Code
import numpy as np
from scipy.optimize import curve_fit as scipy_curve_fit
import math
def adsorption(xdata, ydata, adsorption_model):
"""
Fits adsorption models to data using scipy.optimize.curve_fit. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html for details.
See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html
This example function is provided as-is without any representation of accuracy.
Args:
xdata (list[list]): The xdata value
ydata (list[list]): The ydata value
adsorption_model (str): The adsorption_model value Valid options: Brunauer Emmett Teller Multilayer, Brunauer Emmett Teller Simplified, Freundlich Extended Variable Exponent, Gunary Phosphate Soil Adsorption, Langmuir Extended Fractional Power, Langmuir Extended Reciprocal Form.
Returns:
list[list]: 2D list [param_names, fitted_values, std_errors], or error string.
"""
def _validate_data(xdata, ydata):
"""Validate and convert both xdata and ydata to numpy arrays."""
for name, arg in [("xdata", xdata), ("ydata", ydata)]:
if not isinstance(arg, list) or len(arg) < 2:
raise ValueError(f"{name}: must be a 2D list with at least two rows")
vals = []
for i, row in enumerate(arg):
if not isinstance(row, list) or len(row) == 0:
raise ValueError(f"{name} row {i}: must be a non-empty list")
try:
vals.append(float(row[0]))
except Exception:
raise ValueError(f"{name} row {i}: non-numeric value")
if name == "xdata":
x_arr = np.asarray(vals, dtype=np.float64)
else:
y_arr = np.asarray(vals, dtype=np.float64)
if x_arr.shape[0] != y_arr.shape[0]:
raise ValueError("xdata and ydata must have the same number of rows")
return x_arr, y_arr
# Model definitions dictionary
models = {
'brunauer_emmett_teller_multilayer': {
'params': ['a', 'b'],
'model': lambda x, a, b: (a * b * x) / (1.0 + (b - 2.0) * x - (b - 1.0) * np.square(x)),
'guess': lambda xa, ya: (float(np.mean(ya)), 1.0),
},
'brunauer_emmett_teller_simplified': {
'params': ['a', 'b'],
'model': lambda x, a, b: x / (a + b * x - (a + b) * np.square(x)),
'guess': lambda xa, ya: (1.0, 1.0),
},
'freundlich_extended_variable_exponent': {
'params': ['a', 'b', 'c'],
'model': lambda x, a, b, c: a * np.power(x, b * np.power(x, -c)),
'guess': lambda xa, ya: (1.0, 1.0, 0.5),
},
'gunary_phosphate_soil_adsorption': {
'params': ['a', 'b', 'c'],
'model': lambda x, a, b, c: np.divide(x, a + b * x + c * np.sqrt(np.clip(x, 0.0, None)), out=np.zeros_like(x, dtype=float), where=(a + b * x + c * np.sqrt(np.clip(x, 0.0, None))) != 0),
'guess': lambda xa, ya: (1.0, 1.0, 0.1),
},
'langmuir_extended_fractional_power': {
'params': ['a', 'b', 'c'],
'model': lambda x, a, b, c: (a * b * np.power(x, 1.0 - c)) / (1.0 + b * np.power(x, 1.0 - c)),
'guess': lambda xa, ya: (1.0, 1.0, 0.5),
},
'langmuir_extended_reciprocal_form': {
'params': ['a', 'b', 'c'],
'model': lambda x, a, b, c: 1.0 / (a + b * np.power(x, c - 1.0)),
'guess': lambda xa, ya: (1.0, 1.0, 0.5),
}
}
# Validate model parameter
if adsorption_model not in models:
return f"Invalid model: {str(adsorption_model)}. Valid models are: {', '.join(models.keys())}"
model_info = models[adsorption_model]
# Validate and convert input data
try:
x_arr, y_arr = _validate_data(xdata, ydata)
except ValueError as e:
return f"Invalid input: {e}"
# Perform curve fitting
try:
p0 = model_info['guess'](x_arr, y_arr)
bounds = model_info.get('bounds', (-np.inf, np.inf))
if bounds == (-np.inf, np.inf):
popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, maxfev=10000)
else:
popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, bounds=bounds, maxfev=10000)
fitted_vals = [float(v) for v in popt]
for v in fitted_vals:
if math.isnan(v) or math.isinf(v):
return "Fitting produced invalid numeric values (NaN or inf)."
except ValueError as e:
return f"Initial guess error: {e}"
except Exception as e:
return f"curve_fit error: {e}"
# Calculate standard errors
std_errors = None
try:
if pcov is not None and np.isfinite(pcov).all():
std_errors = [float(v) for v in np.sqrt(np.diag(pcov))]
except Exception:
pass
return [model_info['params'], fitted_vals, std_errors] if std_errors else [model_info['params'], fitted_vals]